import { CreateHandleTo, IHandle, RemoveHandle } from 'babylon-lib/data/data-handle';
import { AnimationFrame, AnimationGroup, Skeleton, TransformNode } from 'babylon-lib/index';
import { Utils } from 'babylon-lib/tool/utils';


interface ICombineRigOptions {
    animationGroup?: AnimationGroup,
    skeletons?: Skeleton[],
    frameLimit?: number
}


class CombineRig {

    public readonly id: number = Utils.id;
    public readonly name: string;

    private _options: ICombineRigOptions;
    private _animationMap: Array<TransformNode> = [];
    private _skeletonMaps: Map<string, Array<TransformNode>> = new Map();
    private _isPlaying: boolean = false;
    private _isLoop: boolean = false;
    private _weight: number = 0;
    private _req = {};
    private _mapping!: { (a: string, b: string): boolean };
    private _localUpdate: boolean = true;
    private _playHandles: IHandle<{ (): void }>[] = [];
    private _pauseHandles: IHandle<{ (): void }>[] = [];
    private _endHandles: IHandle<{ (): void }>[] = [];
    private _loopHandles: IHandle<{ (): void }>[] = [];

    constructor(name: string, options?: ICombineRigOptions) {
        this.name = name;
        this._options = options || {};
        this._mapping = (a, b) => {
            const aa = a.split(/[^a-zA-Z0-9_]/);
            const ab = b.split(/[^a-zA-Z0-9_]/);
            return aa[aa.length - 1] === ab[ab.length - 1];
        };

        this.setAnimationGroup(this._options.animationGroup);
        this.addSkeleton(...this._options.skeletons || []);
    }

    public get animationMap(): Array<TransformNode> {
        return this._animationMap;
    }

    public get skeletonMaps(): Map<string, Array<TransformNode>> {
        return this._skeletonMaps;
    }

    public get isPlaying(): boolean {
        return this._isPlaying;
    }

    public get isLoop(): boolean {
        return this.isLoop;
    }

    public get weight(): number {
        return this._weight;
    }

    public set weight(weight: number) {
        this._weight = Math.max(0, weight);
    }

    public get duration(): number {
        console.log(this._options.animationGroup)
        return this._options.animationGroup ? this._options.animationGroup.to - this._options.animationGroup.from : 0;
    }

    public set localUpdate(localUpdate: boolean) {
        this._localUpdate = localUpdate;
        if (!localUpdate) this.stop();
    }

    public addSkeleton(...skeletons: Skeleton[]): void {
        if (!this._options.skeletons) this._options.skeletons = [];
        skeletons.forEach(skeleton => {
            this.setSkeletonMap(skeleton);
            this._options.skeletons!.push(skeleton);
        });
    }

    public removeSkeleton(...skeletons: Skeleton[]): void {
        if (!this._options.skeletons) return;
        if (!this._options.skeletons.length) return;

        if (skeletons.length) {
            skeletons.forEach(skeleton => {
                this._options.skeletons!.splice(this._options.skeletons!.indexOf(skeleton), 1);
                this._skeletonMaps.delete(skeleton.uniqueId + '');
            });
        } else {
            this._options.skeletons.forEach(skeleton => {
                this._skeletonMaps.delete(skeleton.uniqueId + '');
            });
            this._options.skeletons.length = 0;
        }
    }

    public onPlay(callback: { (): void }): IHandle<{ (): void }> {
        return CreateHandleTo<{ (): void }>(callback, this._playHandles);
    }

    public removePlayHandle(handle: number | IHandle<{ (): void }>): void {
        RemoveHandle(handle, this._playHandles);
    }

    public onPause(callback: { (): void }): IHandle<{ (): void }> {
        return CreateHandleTo<{ (): void }>(callback, this._pauseHandles);
    }

    public removePauseHandle(handle: number | IHandle<{ (): void }>): void {
        RemoveHandle(handle, this._pauseHandles);
    }

    public onEnd(callback: { (): void }): IHandle<{ (): void }> {
        return CreateHandleTo<{ (): void }>(callback, this._endHandles);
    }

    public removeEndHandle(handle: number | IHandle<{ (): void }>): void {
        RemoveHandle(handle, this._endHandles);
    }

    public onLoop(callback: { (): void }): IHandle<{ (): void }> {
        return CreateHandleTo<{ (): void }>(callback, this._loopHandles);
    }

    public removeLoopHandle(handle: number | IHandle<{ (): void }>): void {
        RemoveHandle(handle, this._loopHandles);
    }

    public setAnimationGroup(animationGroup?: AnimationGroup): void {
        this.stop();
        if (this._animationMap.length > 0) {
            this._options.animationGroup!.onAnimationGroupPlayObservable.removeCallback(this.onAnimationGroupPlay.bind(this));
            this._options.animationGroup!.onAnimationGroupPauseObservable.removeCallback(this.onAnimationGroupPause.bind(this));
            this._options.animationGroup!.onAnimationGroupEndObservable.removeCallback(this.onAnimationGroupEnd.bind(this));
            this._options.animationGroup!.onAnimationGroupLoopObservable.removeCallback(this.onAnimationGroupLoop.bind(this));
            this._options.animationGroup = undefined;
        }
        if (!animationGroup) return;
        animationGroup.onAnimationGroupPlayObservable.add(this.onAnimationGroupPlay.bind(this));
        animationGroup.onAnimationGroupPauseObservable.add(this.onAnimationGroupPause.bind(this));
        animationGroup.onAnimationGroupEndObservable.add(this.onAnimationGroupEnd.bind(this));
        animationGroup.onAnimationGroupLoopObservable.add(this.onAnimationGroupLoop.bind(this));
        this._options.animationGroup = animationGroup;
        this.setAnimationMap();
    }

    private onAnimationGroupPlay(): void {
        this._playHandles.forEach(handle => handle.callback());
    }

    private onAnimationGroupPause(): void {
        this._pauseHandles.forEach(handle => handle.callback());
    }

    private onAnimationGroupEnd(): void {
        this._endHandles.forEach(handle => handle.callback());
    }

    private onAnimationGroupLoop(): void {
        this._loopHandles.forEach(handle => handle.callback());
    }

    public set mappingFunction(func: { (a: string, b: string): boolean }) {
        this._mapping = func;
    }

    public get mappingFunction(): { (a: string, b: string): boolean } {
        return this._mapping;
    }

    public setAnimateState(state: 'play' | 'pause' | 'stop', loop?: boolean): void {
        if (!this._options.animationGroup) return;
        switch (state) {
            case 'play':
                this._options.animationGroup.play(loop);
                this._isPlaying = true;
                this.update();
                break;
            case 'pause':
                this._options.animationGroup.pause();
                this._isPlaying = false;
                break;
            case 'stop':
                this._options.animationGroup.stop();
                this._isPlaying = false;
                break;
        }
    }

    public play(loop?: boolean): void {
        if (!this._options.animationGroup) return;
        this._isLoop = !!loop;
        this._options.animationGroup.play(this._isLoop);
        this.update();
        this._isPlaying = true;
    }

    public pause(): void {
        if (this._options.animationGroup) this._options.animationGroup.pause();
        AnimationFrame.cancel(this._req);
        this._isPlaying = false;
    }

    public stop(): void {
        if (this._options.animationGroup) this._options.animationGroup.stop();
        AnimationFrame.cancel(this._req);
        this._isPlaying = false;
    }

    private update(): void {
        if (AnimationFrame.isRunning(this._req)) {
            return;
        }
        if (!this._localUpdate) return;
        const frame = this._options.frameLimit || 16;
        AnimationFrame.request(() => {
            for (let i = 0; i < this._animationMap.length; i++) {
                const aniNode = this._animationMap[i];
                this._skeletonMaps.forEach(skeletonMap => {
                    const boneNode = skeletonMap[i];
                    if (boneNode) {
                        boneNode.position.copyFrom(aniNode.position);
                        boneNode.rotationQuaternion!.copyFrom(aniNode.rotationQuaternion!);
                        boneNode.scaling.copyFrom(aniNode.scaling);
                    }
                });
            }
        }, frame, frame + 200, this._req);

    }

    private setAnimationMap(): void {
        this._animationMap.length = 0;
        this._options.animationGroup!.targetedAnimations.forEach(targetedAnimation => {
            if (!this._animationMap.includes(targetedAnimation.target)) this._animationMap.push(targetedAnimation.target);
        });
        this._options.animationGroup!.stop();
    }

    private setSkeletonMap(skeleton: Skeleton): void {
        if (this._animationMap.length === 0) {
            console.error('animation map is empty');
            return;
        }
        const tmpAry: TransformNode[] = [];

        for (let i = 0; i < this._animationMap.length; i++) {
            const aniNode = this._animationMap[i];
            const bone = skeleton.bones.find(b => this._mapping(b.name, aniNode.name));
            if (bone) {
                tmpAry[i] = bone.getTransformNode()!;
            }
        }
        this._skeletonMaps.set(skeleton.uniqueId + '', tmpAry);
    }

}


export { CombineRig };